package phasebook.impl;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import lottery.RemoteLotteryGameService;
import lottery.msgs.LotteryBet;
import lottery.msgs.LotteryResult;
import lottery.LotteryGameWSClientFactory;
import bank.BankWSClientFactory;
import bank.RemoteBankService;
import bank.msgs.BankTransfer;

import phasebook.client.RemotePhasebookService;
import phasebook.client.RemotePhasebookService.LocalPhaseBookService;
import phasebook.client.msgs.MessageInfo;
import phasebook.client.msgs.MovementInfo;
import phasebook.client.msgs.PhotoInfo;
import phasebook.client.msgs.UserInfo;
import phasebook.client.msgs.UserProfileInfo;
import phasebook.impl.entities.FinancialMovement;
import phasebook.impl.entities.Friendship;
import phasebook.impl.entities.Invitation;
import phasebook.impl.entities.Message;
import phasebook.impl.entities.Photo;
import phasebook.impl.entities.User;

@Stateless
public class PhasebookServiceImpl implements RemotePhasebookService,LocalPhaseBookService{
	@PersistenceContext(unitName="phasebook")
	EntityManager em;
		
	RemoteLotteryGameService lotteryService = LotteryGameWSClientFactory.create("http://localhost:8080/phasebook_esb/http/Phasebook/WebWsEntry");
	
	RemoteBankService bankService =  BankWSClientFactory.create("http://localhost:8080/phasebook_esb/http/Phasebook/WebWsEntry");
	
	@EJB
	private MailSender mailSender;

	@Override
	public UserInfo[] getFriends(long userId) {
		
		// Retrieve the user
		User user = em.find(User.class, userId);
		
		// Check if the user has friends
		if(user.getAcceptedFriends().isEmpty() && user.getAccepterFriends().isEmpty()){
			return null;
		}
		
		// Create the array to return the friends
		UserInfo[] friendsArray = new UserInfo[user.getAcceptedFriends().size() + user.getAccepterFriends().size()];
		         
		// Define the index
		int index = 0;
		
		// Iterate through the friendships where the user was the invited
		for(Friendship friend : user.getAcceptedFriends()){
			
			// Create a new object for the retrieved friend
			UserInfo friendInfo = new UserInfo(friend.getAcceptee().getId(), friend.getAcceptee().getName());
			
			// Add the retrieved friend
			friendsArray[index] = friendInfo;			
			
			// Update the index for the next friend to be added
			index++;
		}

		// Iterate through the friendships where the user invited
		for(Friendship friend : user.getAccepterFriends()){
			
			// Create a new object for the retrieved friend
			UserInfo friendInfo = new UserInfo(friend.getAccepter().getId(), friend.getAccepter().getName());
			
			// Add the retrieved friend
			friendsArray[index] = friendInfo;			
			
			// Update the index for the next friend to be added
			index++;			
		}
		
		return friendsArray;
	}

	@Override
	public UserInfo[] getFriendRequests(long userId) {

		// Retrieve the user
		User user = em.find(User.class, userId);
		
		// Check if the user has invitations pending
		if(user.getInviterFriends().isEmpty()){
			return null;
		}
		
		// Create the array to return the pending invitations for the user
		UserInfo[] inviterArray = new UserInfo[user.getInviterFriends().size()];
		         
		// Define the index
		int index = 0;
		
		// Iterate through the invitations where the user was the invited
		for(Invitation invitation : user.getInviterFriends()){
			
			// Create a new object for the retrieved friend
			UserInfo inviterInfo = new UserInfo(invitation.getInviter().getId(), invitation.getInviter().getName());
			
			// Add the retrieved friend
			inviterArray[index] = inviterInfo;			
			
			// Update the index for the next friend to be added
			index++;
		}
		
		return inviterArray;
	}

	@Override
	public UserInfo[] getMyFriendRequests(long userId) {
		
		// Retrieve the user
		User user = em.find(User.class, userId);
		
		// Check if the user has invitations
		if(user.getInvitedFriends().isEmpty()){
			return null;
		}
		
		// Create the array to return the invitations the user made
		UserInfo[] invitedArray = new UserInfo[user.getInvitedFriends().size()];
		         
		// Define the index
		int index = 0;
		
		// Iterate through the invitations the user made
		for(Invitation invitation : user.getInvitedFriends()){
			
			// Create a new object for the retrieved friend
			UserInfo invitedInfo = new UserInfo(invitation.getInvitee().getId(), invitation.getInvitee().getName());
			
			// Add the retrieved friend
			invitedArray[index] = invitedInfo;			
			
			// Update the index for the next friend to be added
			index++;
		}
		
		return invitedArray;
	}

	@Override
	public MovementInfo[] getFinancialMovements(long userId) {

		// Retrieve the user
		User user = em.find(User.class, userId);

		// Check if the user has movements
		if(user.getMovements().isEmpty()){
			return null;
		}
		
		// Create the array to return the user's movements
		MovementInfo[] movementArray = new MovementInfo[user.getMovements().size()];
		         
		// Define the index
		int index = 0;
		
		// Iterate through the user's movements
		for(FinancialMovement movement : user.getMovements()){
			
			// Create a new object for the retrieved movement
			MovementInfo movementInfo = new MovementInfo(movement.getId(), movement.getAmount(), movement.getDescription(), movement.getTimestamp());
			
			// Add the retrieved movement
			movementArray[index] = movementInfo;			
			
			// Update the index
			index++;
		}
		
		return movementArray;		
	}

	@Override
	public MessageInfo[] getMessages(long myId,long targetUserId) {

		// Retrieve the user
		User user = em.find(User.class, targetUserId);

		boolean includePrivate = true;
		
		if (myId != targetUserId){
			includePrivate = false;
			User me = em.find(User.class, myId);
			for(Friendship f: me.getAcceptedFriends()){
				if (f.getAcceptee().getId()==targetUserId){
					includePrivate = true;
					break;
				}
			}
			for(Friendship f: me.getAccepterFriends()){
				if (f.getAccepter().getId()==targetUserId){
					includePrivate = true;
					break;
				}
			}
		}
		
		// Check if the user has messages
		if(user.getBoard().isEmpty()){
			return null;
		}
		
		// Define the array of messages to return
		ArrayList<MessageInfo> retMsgs = new ArrayList<MessageInfo>();
		
		// Iterate through the returned messages and convert them into message info
		for(Message retrievedMessage : user.getBoard()){
							
			if( (retrievedMessage.isPrivate() && includePrivate) || !retrievedMessage.isPrivate() ){
				// Create a new message info object for the retrieved message
				MessageInfo newMessageInfo = new MessageInfo();
				
				// Create a new user info object for the publisher
				UserInfo publisher = new UserInfo(retrievedMessage.getPublisher().getId(), retrievedMessage.getPublisher().getName());
	
				// Create a new user info object for the board owner
				UserInfo owner = new UserInfo(retrievedMessage.getBoardOwner().getId(), retrievedMessage.getBoardOwner().getName());
				
				// Create a container for the photo
				PhotoInfo photo = null;
				
				// Check if the message has a photo
				if(retrievedMessage.getPhoto() != null){
					// Create a new photo info object for the photo
					photo = new PhotoInfo(retrievedMessage.getPhoto().getId(),  retrievedMessage.getPhoto().getUrl(),  retrievedMessage.getPhoto().getUploadTimestamp());
				}
				
				// Define the parameters of the new message info
				newMessageInfo.setMessaegId(retrievedMessage.getId());
				newMessageInfo.setBoardOwner(owner);
				newMessageInfo.setMessageText(retrievedMessage.getText());
				newMessageInfo.setPublisher(publisher);
				newMessageInfo.setTimestamp(retrievedMessage.getPublishTimestamp());
				newMessageInfo.setPhoto(photo);
				newMessageInfo.setPrivate(retrievedMessage.isPrivate());
				
				// Add the new message info to the array to return
				retMsgs.add(newMessageInfo);
			}
		}
		
		return retMsgs.toArray(new MessageInfo[retMsgs.size()]);
	}

	@Override
	public UserProfileInfo getProfile(long userId) {
		
		// Retrieve the user
		User user = em.find(User.class, userId);

		// Create and retrieve the profile
		UserProfileInfo profile = new UserProfileInfo(userId, user.getName(), user.getName(), user.getPassword(), user.getEmail(), user.getBalance(), user.getBankAccountId());
		
		return profile;
	}

	@Override
	public PhotoInfo[] getAlbum(long userId) {

		// Retrieve the user
		User user = em.find(User.class, userId);

		// Check if the user has photos
		if(user.getAlbum().isEmpty()){
			return null;
		}
		
		// Define the array of photos to return
		PhotoInfo[] photoArray = new PhotoInfo[user.getAlbum().size()];
		
		// Define an index to be used while populating the array
		int index = 0;
		
		for(Photo retrievedPhoto : user.getAlbum()){
			
			// Create a new photo info object for the retrieved photo
			PhotoInfo newPhotoInfo = new PhotoInfo();
			
			// Define the parameters of the new photo info
			newPhotoInfo.setPhotoId(retrievedPhoto.getId());
			newPhotoInfo.setUploadTimestamp(retrievedPhoto.getUploadTimestamp());
			newPhotoInfo.setUrl(retrievedPhoto.getUrl());
			
			// Add the new photo info to the array to return
			photoArray[index] = newPhotoInfo;
		
			// Update index to add the next photo
			index++;
		}
		
		return photoArray;
	}

	@Override
	public UserInfo[] findUsers(long userId, String regEx) throws Exception {
		
		// Define a query and retrieve the user with the specified user name
		Query query = em.createNamedQuery("user.findUserbyRegEx");
		query.setParameter("regEx", regEx);
		
		@SuppressWarnings("unchecked")
		List<User> users = query.getResultList();
		
		//Check if the search returned users
		if(users.isEmpty()){
			throw new Exception("No users found.");
		}
		
		// Define container for users found
		UserInfo[] retrievedUsers = new UserInfo[users.size()]; 
		int index =0;
		
		// Populate the container with the retrieved users
		for(User user : users){
			UserInfo retrievedUser = new UserInfo(user.getId(), user.getName());  
			
			retrievedUsers[index] = retrievedUser;
			
			index++;
		}
		
		return retrievedUsers;
	}
	
	@Override
	public long registerUser(UserProfileInfo userProfile) throws Exception {
		
		// Create and set the parameters of the new user
		User user = new User();

		user.setName(userProfile.getName());
		user.setUsername(userProfile.getUsername());
		user.setPassword(userProfile.getPassword());
		user.setEmail(userProfile.getEmail());
		user.setBalance(userProfile.getBalance());
		user.setBankAccountId(userProfile.getBankAccountId());
		
		// Update the database
		try{
			em.persist(user);			
		}catch(Exception e){
			throw new Exception("Error creating new user in the database.", e);
		}
		
		// Prepare and send email to user
		mailSender.postMail("Dear " + user.getName() + ", welcome to Phasebook", "[PHASEBOOK] - Welcome to Phasebook", user.getEmail());
		
		return user.getId();
	}	
	
	@Override
	public void updateUser(UserProfileInfo userProfile) throws Exception {

		// Retrieve the user
		User user = em.find(User.class, userProfile.getUserId());

		// Update the user profile
		user.setName(userProfile.getName());
		user.setUsername(userProfile.getUsername());
		user.setPassword(userProfile.getPassword());
		user.setEmail(userProfile.getEmail());
		user.setBankAccountId(userProfile.getBankAccountId());
		
//		// Check if there was a new financial movement
//		if( user.getBalance() != userProfile.getBalance()){
//						
//			// Create a financial movement
//			FinancialMovement newMovement = new FinancialMovement();
//			newMovement.setAmount(userProfile.getBalance() - user.getBalance());
//			newMovement.setDescription("Money charge");
//			newMovement.setTimestamp(new Date(System.currentTimeMillis()));
//			newMovement.setUser(user);
//			
//			// Update balance
//			user.setBalance(userProfile.getBalance());
//			
//			// Update the database with the new movement
//			try {
//				em.persist(newMovement);
//			} catch (Exception e) {
//				throw new Exception("Error updating a new movement in the database.", e);
//			}			
//		}
			
		//Update the database
		try {
			em.persist(user);
		} catch (Exception e) {
			throw new Exception("Error updating the user in the database.", e);
		}		
	}

	@Override
	public long validateLogin(String userName, String password) throws Exception {

		// Define a query and retrieve the user with the specified user name
		Query query = em.createNamedQuery("user.findByUserName");
		query.setParameter("username", userName);
		
		User retrievedUser = null;
		
		// Check the validity of the submitted user name
		try{
			retrievedUser = (User) query.getSingleResult();
		}catch(NoResultException e){
			throw new Exception("Invalid user name.");
		}
		
		// Check the validity of the submitted password
		if(!retrievedUser.getPassword().equals(password)){
			throw new Exception("Invalid password.");
		}
		
		return retrievedUser.getId();
	}

	@Override
	public void publishMessage(MessageInfo newMessage) throws Exception {
		
		// Retrieve the publisher
		User publisher = em.find(User.class, newMessage.getPublisher().getUserId());
		
		// Retrieve the board owner
		User boardOwner = em.find(User.class, newMessage.getBoardOwner().getUserId());

		// Define a container for the photo
		Photo photo = null;
		
		// Check if message contains a photo
		if(newMessage.getPhoto() != null){
			// Retrieve the photo
			photo = em.find(Photo.class, newMessage.getPhoto().getPhotoId());		
		}
		// Create a new message
		Message message = new Message();

		// Define the parameters of the message
		message.setBoardOwner(boardOwner);
		message.setText(newMessage.getMessageText());
		message.setPublisher(publisher);
		message.setPublishTimestamp(newMessage.getTimestamp());
		message.setPrivate(newMessage.isPrivate());

		// Check if message contains a photo
		if(photo != null){
			message.setPhoto(photo);
		}
		else{
			message.setPhoto(null);
		}
		
		//Update the database
		try {
			em.persist(message);
		} catch (Exception e) {
			throw new Exception("Error creating a new message in the database.", e);
		}
		
		// Prepare and send email to user
		if(boardOwner.getId() != publisher.getId()){
			mailSender.postMail("Dear " + boardOwner.getName() + ", you have a new message from " + publisher.getName() + ".", "[PHASEBOOK] - New board message", boardOwner.getEmail());
		}
	}

	@Override
	public void deleteMessage(long userId, long messageId) throws Exception {

		// Retrieve the message 
		Message deletedMessage = em.find(Message.class, messageId);
	
		// Check if the user is the can delete the message
		if(deletedMessage.getPublisher().getId() != userId){
			throw new Exception("The user cannot delete the message.");
		}
		
	    // Delete the message from the database
		try {
		    em.remove(deletedMessage); 
		} catch (Exception e) {
			throw new Exception("Error deleting invitation(cancel) from the database.", e);
		}
		
	}

	@Override
	public void requestFriendship(long userId, long friendId) throws Exception {
		
		// Retrieve the inviter user
		User inviter = em.find(User.class, userId);

		// Retrieve the invitee user
		User invitee = em.find(User.class, friendId);		
		
		// Create a new invitation
		Invitation newInvitation = new Invitation();

		// Define the parameters of the invitation
		newInvitation.setInviter(inviter);		
		newInvitation.setInvitee(invitee);
		
		//Update the database
		try {
			em.persist(newInvitation);
		} catch (Exception e) {
			throw new Exception("Error creating a new invitation in the database.", e);
		}
		
		// Prepare and send email
		mailSender.postMail("Dear " + invitee.getName() + ", " + inviter.getName() + " wants to be your friend.", "[PHASEBOOK] - Friendship request", invitee.getEmail());
		
	}

	@Override
	public void cancelFriendship(long userId, long friendId) throws Exception {

		// Retrieve the user
		User user = em.find(User.class, userId);
		
		// Find the friendship
		long friendshipId = -1;
		
		for( Friendship friendship : user.getAcceptedFriends() ){
			if( friendship.getAcceptee().getId() == friendId ){
				friendshipId = friendship.getId();
				break;
			}
		}
		
		// If the friend is not in the list of accepted check the list of accepters
		if(friendshipId == -1){
			for( Friendship friendship : user.getAccepterFriends() ){
				if( friendship.getAccepter().getId() == friendId ){
					friendshipId = friendship.getId();
					break;
				}
			}
		}	
		
		// Check if the friendship was not deleted meanwhile
		if(friendshipId == -1){
			throw new Exception("Friendship does not exists anymore.");
		}

		// Retrieve the friendship
		Friendship cancelledFriendship = em.find(Friendship.class, friendshipId);
	
	    // Delete invitation from the database
		try {
		    em.remove(cancelledFriendship); 
		} catch (Exception e) {
			throw new Exception("Error deleting invitation(cancel) from the database.", e);
		}
		
		// Retrieve the former friend user
		User exFriend = em.find(User.class, friendId);

		// Prepare and send email
		mailSender.postMail("Dear " + exFriend.getName() + ", " + user.getName() + " is no longer your friend.", "[PHASEBOOK] - Friendship cancellation", exFriend.getEmail());
		
	}

	@Override
	public void cancelFriendshipRequest(long userId, long friendId) throws Exception {
		
		// Retrieve the user
		User user = em.find(User.class, userId);
		
		// Find the invitation		
		long invitationId = -1;
		
		for( Invitation inv : user.getInvitedFriends() ){
			if( inv.getInvitee().getId() == friendId ){
				invitationId = inv.getId();
				break;
			} 
		}
		
		// Check if the invitation was not deleted meanwhile
		if(invitationId == -1){
			throw new Exception("Invitation does not exists anymore.");
		}

		// Retrieve the invitation		
		Invitation cancelledInvitation = em.find(Invitation.class, invitationId);
	
	    // Delete invitation from the database
		try {
		    em.remove(cancelledInvitation); 
		} catch (Exception e) {
			throw new Exception("Error deleting invitation(cancel) from the database.", e);
		}
		
	}

	@Override
	public void denyFriendshipRequest(long userId, long friendId) throws Exception {
		
		// Retrieve the user
		User user = em.find(User.class, userId);
		
		// Find the invitation		
		long invitationId = -1;
		
		for( Invitation inv : user.getInviterFriends() ){
			if( inv.getInviter().getId() == friendId ){
				invitationId = inv.getId();
				break;
			}
		}
		
		// Check if the invitation was not deleted meanwhile
		if(invitationId == -1){
			throw new Exception("Invitation does not exists anymore.");
		}

		// Retrieve the invitation		
		Invitation deniedInvitation = em.find(Invitation.class, invitationId);
		
	    // Delete invitation from the database
		try {
		    em.remove(deniedInvitation); 
		} catch (Exception e) {
			throw new Exception("Error deleting invitation(deny) from the database.", e);
		}
		
	}

	@Override
	public void acceptFriendshipRequest(long userId, long friendId) throws Exception {
		
		// Retrieve the uploading user
		User user = em.find(User.class, userId);

		// Retrieve the uploading user
		User friend = em.find(User.class, friendId);
		
		// Find the invitation		
		long invitationId = -1;
		
		for( Invitation inv : user.getInviterFriends() ){
			if( inv.getInviter().getId() == friendId ){
				invitationId = inv.getId();
				break;
			} 
		}
		
		// Check if the invitation was not deleted meanwhile
		if(invitationId == -1){
			throw new Exception("Invitation does not exists anymore.");
		}

		// Retrieve the invitation		
		Invitation acceptedInvitation = em.find(Invitation.class, invitationId);
		
		// Create and the define the parameters of the new Friendship
		Friendship newFriendship = new Friendship();
		
		newFriendship.setAccepter(user);
		newFriendship.setAcceptee(friend);	
				
	    // Delete invitation from the database
		try {
		    em.remove(acceptedInvitation); 
		} catch (Exception e) {
			throw new Exception("Error deleting invitation(accepted) from the database.", e);
		}		
		
		// Update the database with the new friendship
		try {
			em.persist(newFriendship);
		} catch (Exception e) {
			throw new Exception("Error creating a new frienship in the database.", e);
		}
		
		// Prepare and send email
		mailSender.postMail("Dear " + friend.getName() + ", " + user.getName() + " has accepted your friendship request.", "[PHASEBOOK] - Friendship accepted", friend.getEmail());
		
	}

	@Override
	public void addPhoto(long userId, PhotoInfo photo,byte imageData[]) throws Exception {

		// Retrieve the uploading user
		User user = em.find(User.class, userId);
		
		// Create a new photo
		Photo newPhoto = new Photo();

		// Define the parameters of the photo
		newPhoto.setOwner(user);
		newPhoto.setUploadTimestamp(photo.getUploadTimestamp());
		newPhoto.setUrl(photo.getUrl());
		newPhoto.setData(imageData);
		
		//Update the database
		try {
			em.persist(newPhoto);
		} catch (Exception e) {
			throw new Exception("Error updating the database with new photo.", e);
		}		
	}

	@Override
	public void placeBet(long userId, double amount, int result) throws Exception {
		
		updateBalance(userId, -amount, 0);
		createFinancialMovement(userId, -amount, "[LOTTERY BET] [BET ON RESULT: " + result + "]");
		
		// Prepare the bet to be placed
		LotteryBet newBet = new LotteryBet();
		
		newBet.setTokenId(userId);
		newBet.setAmountBetted(amount);
		newBet.setResultBetted(result);
		newBet.setSourceSystemId(1); //PHASEBOOK SYSTEM ID = 1
		
		// Place the bet through the betting service
		lotteryService.placeBet(newBet);
		
		
		BankTransfer bankTransfer = new BankTransfer();
		
		// Phasebook Bank Account HARDCODED VALUE 1 (Exising account is assumed) 
		bankTransfer.setFromAccountId(1);
		
		// Lottery Bank Account HARDCODED VALUE 2 (Exising account is assumed)
		bankTransfer.setToAccountId(2);
		
		bankTransfer.setTransferAmount(amount);
		
		bankService.validateTransferRequest(bankTransfer);
		bankService.requestTransfer(bankTransfer);
	}

	@Override
	public byte[] downloadPhoto(long photoId) {
		return em.getReference(Photo.class, photoId).getData();
	}

	@Override
	public void updateBalance(long userId, double amount, long accountId) throws Exception {

		// Retrieve the user that will have his balance updated
		User bettingUser = em.find(User.class, userId);
		
		// Calculate the new balance
		double newBalance = bettingUser.getBalance() + amount; 
		
		//Update the database with new user's balance
		try {
			bettingUser.setBalance(newBalance);
			em.persist(bettingUser);
		} catch (Exception e) {
			throw new Exception("Error updating the database with new user's balance.", e);
		}		
	}

	@Override
	public void createFinancialMovement(long userId, double amount,	String description) throws Exception {
		
		// Retrieve the user that is associated with the new financial movement
		User accountUser = em.find(User.class, userId);
		
		// Register the financial movement
		FinancialMovement newMovement = new FinancialMovement(); 
		newMovement.setAmount(amount);
		newMovement.setUser(accountUser);
		newMovement.setTimestamp(new Date(System.currentTimeMillis()));
		newMovement.setDescription(description);
		
		//Update the database with new movement
		try {
			em.persist(newMovement);
		} catch (Exception e) {
			throw new Exception("Error updating the database with new debit movement.", e);
		}		
		
	}

	@Override
	public void processResults(LotteryResult[] lotteryResults)
			throws Exception {
		
		// Update the winners balance
		for(LotteryResult result : lotteryResults){
			
			// Check if the better is a winner
			if(result.getAmountDue() > 0 ){
				
				// Create the associated financial movement
				createFinancialMovement(result.getTokenId(), result.getAmountDue(), result.getLotteryDescription());

				// Update the winner's balance 
				updateBalance(result.getTokenId(), result.getAmountDue(), 0);
				
			}
		}
	
	}
	
	@Override
	public void notifyLotteryParticipants(LotteryResult[] lotteryResults) throws Exception {

		// Notify lottery participants
		for(LotteryResult result : lotteryResults){

			// Retrieve the winner
			User lotteryParticipant = em.find(User.class, result.getTokenId());
			
			// Prepare and send email to user
			String	greeting = "Dear ";
			if(result.isWinner()){
				greeting = "Congratulations ";
			}
			
			mailSender.postMail(greeting + lotteryParticipant.getName() + ", the result of lottery {"+ result.getLotteryDescription() +"} is " + result.getResult()+ ", and your bet was " + result.getResultBetted() + ".", "[PHASEBOOK] - Lottery result", lotteryParticipant.getEmail());
		}
		
	}
	
}
